'''DynamicTableCtrl - a dynamic table control

This was inspired by the table editing features of emacs' org-mode. Based
on a StyledTextCtrl, you create table cells by inserting vertical bars. Otherwise
it works more-or-less like a text editor. 

'''

import wx
import wx.stc
import wx.lib.newevent
import re
import hashlib
from contextlib import contextmanager

# *sigh* unicode gives me issues (deleting half of a multi-byte character
# sometimes). But when I use the ASCII unit separator the widget insists
# on displaying it in reverse video. But there's a workaround; search
# further down for  STC_STYLE_CONTROLCHAR.
SEP=u"\u001F" # ASCII Unit Separator
STYLE_COMMENT = 1
STYLE_SEP = 2
MARKER_BREAKPOINT = 0
MARKER_CURRENT_BREAKPOINT = 1
ZEBRA_ODD = 2
ZEBRA_EVEN = 3

CellFocusChangedEvent, EVT_CELL_FOCUS_CHANGED = wx.lib.newevent.NewEvent()
BreakpointSetEvent, EVT_BREAKPOINT_SET = wx.lib.newevent.NewEvent()
BreakpointUnsetEvent, EVT_BREAKPOINT_UNSET = wx.lib.newevent.NewEvent()

class DynamicTableCtrl(wx.stc.StyledTextCtrl):
    def __init__(self, parent, id=wx.ID_ANY, *args, **kwargs):
        wx.stc.StyledTextCtrl.__init__(self, parent, id, *args, **kwargs)
        self.auto_columns = True
        self.zebra_stripes = True
        self.count = 0
        self._emacs_bindings = True # should get from settings file, no?
        self.current_cell_index = None
        self._auto_save_func = None
        self._scheduled_jobs = {}
        self._auto_save_enabled = True
        self._md5 = ""

        self.SetLexer(wx.stc.STC_LEX_CONTAINER)
        self.SetCodePage(wx.stc.STC_CP_UTF8)
        self.SetUseAntiAliasing(True)

        self.SetMarginLeft(10)
        self.SetMarginWidth(0,0)
        self.SetMarginWidth(1,0)
        self.SetMarginWidth(2,0) 
        self.SetMarginWidth(3,0)

        self.SetMarginType(1, wx.stc.STC_MARGIN_SYMBOL) 
        self.SetMarginSensitive(1, True)
        # docs say the two colors are foreground and background, but 
        # at least on my linux box it seems to be outline, fill
#        self.MarkerDefine(MARKER_BREAKPOINT, wx.stc.STC_MARK_CIRCLE, "black", "red")
        self.MarkerDefine(MARKER_BREAKPOINT, 
                          wx.stc.STC_MARK_CIRCLE, "black", "red")
        self.MarkerDefine(MARKER_CURRENT_BREAKPOINT, 
                          wx.stc.STC_MARK_SHORTARROW, "black", "red")

        # fixed_font = wx.Font(10, wx.FONTFAMILY_MODERN, wx.NORMAL, wx.NORMAL, False)
        # if not fixed_font.IsFixedWidth():
        #     # I read somewhere that wx.MODERN won't necessarily be
        #     # monoface, but wx.TELETYPE is. 
        #     fixed_font = wx.Font(10, wx.FONTFAMILY_TELETYPE, wx.NORMAL, wx.NORMAL, False)
        default_font = self.GetTopLevelParent().GetFont()
        self.StyleSetSpec(wx.stc.STC_STYLE_DEFAULT, "face:%s,size:%d" % \
                              (default_font.GetFaceName(), default_font.GetPointSize()))

        self.StyleClearAll()
        self.StyleSetSpec(wx.stc.STC_STYLE_LINENUMBER,'fore:#000000,back:#99A9C2')    
        self.StyleSetSpec(STYLE_COMMENT, "fore:#b22222") # firebrick

        # Danger Will Robinson!  StyledTextCtrl seems to insist on
        # showing control characters in reverse video (and as of this
        # writing I'm using a control character as the separator). If
        # I want a specific color to show I'm going to set the
        # background to that, then set the point size to 1 (one) so
        # that all I see is background. The net effect is a thin line.
        self.SetControlCharSymbol(ord("|"))
        self.StyleSetSpec(wx.stc.STC_STYLE_CONTROLCHAR, 'size:1')
        self.StyleSetSpec(STYLE_SEP, "fore:#a2a2a2:size:1")

        # for zebra stripes
        self.MarkerDefine(ZEBRA_EVEN, wx.stc.STC_MARK_BACKGROUND, background="#EBF3FF")
        self.MarkerDefine(ZEBRA_ODD, wx.stc.STC_MARK_BACKGROUND, background="#FFFFFF")

        self.Bind(wx.stc.EVT_STC_MARGINCLICK, self.OnMarginClick)

        # Note that this mapping doesn't include any characters
        # that happen to be menu accelerators. There's just no
        # way to make that happen; wxPython steals those keystrokes
        # before this widget has a chance to process them.
        self._key_table = KeyMap(self, self.OnKeyDown)
        self._key_table.bind(wx.WXK_TAB, self.OnTab)
        self._key_table.bind("shift",wx.WXK_TAB, self.OnShiftTab)
        self._key_table.bind("|", self.OnVerticalBar)
        self._key_table.bind("control", wx.WXK_TAB, self.OnInsertColumn)

        # FIXME: shift-delete seems to work, shift-insert does not. WTF?
        self._key_table.bind("shift",wx.WXK_INSERT, self.OnPaste)
        self._key_table.bind("control",wx.WXK_INSERT, self.OnCopy)
        self._key_table.bind("shift",wx.WXK_DELETE, self.OnCut)
        self._key_table.bind("control","c", self.OnCopy)
        self._key_table.bind("control","v", self.OnPaste)
        self._key_table.bind("control","x", self.OnCut)
        self._key_table.bind("control","+", self.OnZoomIn) # control-plus
        self._key_table.bind("control","=", self.OnZoomIn) # easier alias for control-plus
        self._key_table.bind("control","-", self.OnZoomOut) 
        self._key_table.bind("control","shift","-", self.OnZoomOut) # convenient synonym for control-minus
        self._key_table.bind("control","0", self.OnZoomReset)

#        self._key_table.bind("control"," ", self.OnContentAssistance)

#        if self._emacs_bindings:
#            self.InitEmacsBindings()

        self._auto_save_timer = wx.Timer(self, id=wx.NewId())
        self.Bind(wx.EVT_TIMER, self.OnAutoSaveTimer, self._auto_save_timer)

        mod_mask  = wx.stc.STC_PERFORMED_USER
        mod_mask |= wx.stc.STC_PERFORMED_UNDO|wx.stc.STC_PERFORMED_REDO
        mod_mask |= wx.stc.STC_MOD_INSERTTEXT
        mod_mask |= wx.stc.STC_MOD_DELETETEXT
        mod_mask |= wx.stc.STC_MOD_CHANGEFOLD
        self.SetModEventMask(mod_mask)
        self.Bind(wx.stc.EVT_STC_UPDATEUI, self.OnUpdateUI)
        self.Bind(wx.stc.EVT_STC_CHANGE, self.OnChange)
        self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDown)
    
    @contextmanager
    def AutoSaveDisabled(self):
        orig_value = self._auto_save_enabled
        self._auto_save_enabled = False
        yield
        self._auto_save_enabled = orig_value

    def NormalizeColumns(self):
        print "NormalizeColumns..."
        data = self.GetRows()
        cw = column_widths(data)
        print "column_widths:", cw
#        for n, row in enumerate(data):


    def GetModify(self):
        '''Return True if the data has been modified

        This widget may add extra empty rows or columns to the
        original data. These blank columns and rows are ignored when
        deciding whether the data has changed or not.
        
        '''
        md5 = self.Checksum()
        modified = (self._md5 != md5)
        return modified

    def SetSavePoint(self):
        '''Mark the data as "saved"

        This method also calculates the md5 checksum, which is used by
        GetModify()

        '''
        super(DynamicTableCtrl, self).SetSavePoint()
        self._md5 = self.Checksum()

    def _get_column_widths(self, data):
        # each row may have a different number of columns,
        # so we have to do a little normalization...
        column_widths = [[len(cell) for cell in row] for row in data]
        maxcols = max([len(row) for row in column_widths])
        map(lambda row: row.extend([0]*maxcols), column_widths)
        cw = [row[:maxcols] for row in column_widths]
        return cw

    def OnMarginClick(self, evt):
        if evt.GetMargin() == 1:
            lineClicked = self.LineFromPosition(evt.GetPosition())
            marker = self.MarkerGet(lineClicked)
            breakpoint = marker & (1<<MARKER_BREAKPOINT)
            if breakpoint == 0:
                self.MarkerAdd(lineClicked, MARKER_BREAKPOINT)
                new_event = BreakpointSetEvent(linenumber = lineClicked)
            else:
                self.MarkerDelete(lineClicked, MARKER_BREAKPOINT)
                new_event = BreakpointUnsetEvent(linenumber = lineClicked)
            new_event.SetEventObject(self)
            wx.PostEvent(self, new_event)

    def SetPopup(self, popup):
        self._popup = popup

    def OnZoomIn(self, event):
        factor = min(self.GetZoom() + 1, 20)
        # note to self: save zoom factor
        self.SetZoom(factor)

    def OnZoomOut(self, event):
        factor = max(self.GetZoom() - 1, -10)
        # note to self: save zoom factor
        self.SetZoom(factor)

    def OnZoomReset(self, event):
        factor = 0
        # note to self: save zoom factor
        self.SetZoom(0)

    def OnLeftDown(self, event):
        self.SelectCurrentCell()
        
    def OnAutoSaveTimer(self, event):
        if self.GetModify() and self._auto_save_func is not None:
            self._auto_save_func()

    def BindKey(self, *args):
        self._key_table.bind(*args)
        
    def _get_cell_ranges(self, pos=None, trim_whitespace=False):
        '''return the ranges for cell data for row of given position'''
        if pos is None:
            pos = self.GetCurrentPos()

        line = self.LineFromPosition(pos)
        linestart = self.PositionFromLine(line)
        lineend = self.GetLineEndPosition(line)

        result = []
        next_sep = self.FindText(linestart, lineend, SEP)
        while next_sep != -1:
            extents = self._get_cell_extents(next_sep, trim_whitespace=trim_whitespace)
            result.append(extents)
            next_sep = self.FindText(self.PositionAfter(next_sep), lineend, SEP)
        result.append(self._get_cell_extents(lineend))
            
        return result

        
    def DeleteTrailingWhitespace(self, line=None):
        if line is None:
            line = self.LineFromPosition(self.GetCurrentPos())
        text = self.GetLine(line).rstrip()
        start = self.PositionFromLine(line)
        end = self.GetLineEndPosition(line)
        self.SetSelection(start, end)
        self.ReplaceSelection(text.rstrip())
        
    def AppendColumn(self, line=None):
        if line is None:
            line = self.LineFromPosition(self.GetCurrentPos())
        # save selection, so we can restore it later
        sel = self.GetSelection()

        start = self.PositionFromLine(line)
        end = self.GetLineEndPosition(line)
        text = self.GetLine(line)
        new_text = text.rstrip() + " " + SEP + " "
        self.SetSelection(start, end)
        self.ReplaceSelection(new_text)
        if sel:
            self.SetSelection(*sel)
        
    def do_auto_text(self):
        '''Handle auto-text

        "auto-text" refers to adding a trailing empty column
        and row, but we could do other things here as well.
        '''
        if self.auto_columns:
            (row, column) = self.GetCurrentCell()
            next = self._next_cell()
            ranges = self._get_cell_ranges()
            cells = self.GetCells()

            last = cells[-1].strip()
            if last != "":
                # non-empty last column, so add another empty column
                self.AppendColumn(row)

        # make sure there's always a blank line. Makes life easier.
        lastpos = self.GetLength()-1
        last_char = self.GetChar(lastpos)
        if lastpos >= 0 and last_char != "\n":
            self.AppendText("\n")

    def schedule_job(self, func, *args):
        '''Run a job, or restart an already existing scheduled job'''
        key = (func,) + args
        if key in self._scheduled_jobs and self._scheduled_jobs[key].IsRunning():
            self._scheduled_jobs[key].Restart()
        else:
            self._scheduled_jobs[key] = wx.CallLater(100, func, *args)

    def do_automagic(self):
        self.do_auto_text()
        self._update_zebra_stripes()
        self.AddStyleRegexp(1, self.GetLength(), STYLE_SEP, SEP)

    def OnChange(self, event):
        # do we REALLY want to call this for every change?
#        if self._auto_save_enabled and self._auto_save_func:
#            self._auto_save_func()
        # this work must be done outside of the event handler
        # since it might make changes to the widget
        event.Skip()
        wx.CallAfter(self.do_automagic)

    def OnUpdateUI(self, event):
        prev_cell_index = self.current_cell_index
        self.current_cell_index = self.GetCurrentCell()
        # the following means "if we've moved to another row or column..."
        # NOT "has the value changed?". 
        if prev_cell_index != self.current_cell_index:
            new_event = CellFocusChangedEvent(prev_cell_index=prev_cell_index,
                                              current_cell_index = self.current_cell_index)
#            self.SetFocus() # does this belong here?
            new_event.SetEventObject(self)
            wx.PostEvent(self, new_event)
            self.OnChange(event)
        event.Skip()

    def _update_zebra_stripes(self):
        '''Redraw the zebra stripes and field separators'''
        self.MarkerDeleteAll(ZEBRA_ODD)
        self.MarkerDeleteAll(ZEBRA_EVEN)
        if self.zebra_stripes:
            lines = self.GetLineCount()
            marker = ZEBRA_EVEN
            self._stripes = True
            if self._stripes:
                for line in range(self.GetLineCount()):
                    if self.GetLineVisible(line):
                        linestart = self.PositionFromLine(line)
                        lineend = self.GetLineEndPosition(line)
                        marker = ZEBRA_ODD if marker == ZEBRA_EVEN else ZEBRA_EVEN
                        self.MarkerAdd(line, marker)

    def Checksum(self):
        '''Return a checksum of the canonical form

        Get the canonical form of the data (ie: with trailing blank
        columns and rows removed) and compute the md5 checksum.

        This seems crazy expensive, but this widget is designed to be
        used with a relatively small number of rows. 
        '''
        data = self.GetRows()
        m = hashlib.md5()
        m.update(str(data))
        return m.digest()
        
    def CanCopy(self):
        (start, end) = self.GetSelection()
        return (end > start)

    def CanCut(self):
        (start, end) = self.GetSelection()
        return (end > start)

    def CanPaste(self):
        # if I put code in here to check the clipboard, it seems that
        # the paste button will rapidly switch between enabled and
        # disabled. Why?  wiki.wxpython.org/ClipBoard implies this
        # could also lead to segfaults, so let's just assume there is
        # always something on the clipboard for now.
        return True

    def CanSave(self):
        md5 = self.Checksum()
        if md5 != self._md5:
            return True
        return False

    def Clear(self):
        self.ClearAll()
        self.current_cell_index = None

    def InitEmacsBindings(self):
        '''Implement emacs-like bindings
        
        eventually I want to support:
        ctrl-n, ctrl-p, ctrl-o, ctrl-a, ctrl-e, ctrl-k
        '''
        # I'm sure these will clash with some RIDE menu accelerataors, which
        # I can do nothing about. *sigh*
        emacs = EmacsBindings(self)
        self._key_table.bind("control","a", emacs.beginning_of_line)
        self._key_table.bind("control","e", emacs.move_end_of_line)
        self._key_table.bind("control","k", emacs.kill_line)
        self._key_table.bind("control","n", emacs.next_line)
        self._key_table.bind("control","p", emacs.previous_line)
        self._key_table.bind("control","o", emacs.open_line)
        self._key_table.bind("control","f", emacs.forward_char)
        self._key_table.bind("control","b", emacs.backward_char)
        self._key_table.bind("control","s", self.OnFind)        
        pass

    # Specialized cut/copy/paste converts separators to tabs and visa versa
    def Cut(self):
        self.Copy()
        self.ReplaceSelection("")

    def Copy(self):
        # convert separators to tabs; most external paste targets
        # will expect that (for example excel)
        data = wx.TextDataObject()
        text = self.GetSelectedText().replace(SEP, "\t")
        data.SetText(text)
        if wx.TheClipboard.Open():
            wx.TheClipboard.SetData(data)
            wx.TheClipboard.Close()

    def transmogrify(self, text, filter=None):
        '''Attempt to convert external data so it's usable by the editor
        
        This method looks for html table rows, and also tab-delimited 
        data. If it finds either it makes an attempt to convert it
        to our SEP-separated format
        '''
        if re.match("<tr.*?>.*?<td.*?>", text):
            # It looks like we  have an embedded html table. 
            # Ignore everything but the table (is that _really_
            # what we want to do?)
            newdata = []
            for row in re.finditer("<tr.*?>(.*?)</tr>", text):
                data = row.groups()[0]
                cells = []
                for column in re.finditer("<td.*?>(.*?)</td>", text):
                    cells.append(column.groups()[0])
                newdata.append(SEP.join(cells))
            text = "\n".join(newdata)
        elif text.find("\t") >= 0:
            # embedded tabs?
            text = text.replace("\t",SEP)
        return text

    def Paste(self):
        # convert tabs to separators, since most external copy
        # sources will give us that for tabular data (for
        # example. excel)
        if not self.CanPaste():
            return
        data = wx.TextDataObject()

        success = False
        if wx.TheClipboard.Open():
            success = wx.TheClipboard.GetData(data)
            wx.TheClipboard.Close()
        if success:
            text = self.transmogrify(data.GetText())
            self.ReplaceSelection(text)

    def OnCut(self, event): 
        self.Cut()

    def OnCopy(self, event): 
        self.Copy()

    def OnPaste(self, event): 
        self.Paste()

    def GetChar(self, pos=None):
        '''Returns character at pos

        This properly handles multibyte characters.
        '''
        if pos is None: pos = self.GetCurrentPos()
        next = self.PositionAfter(pos)
        char = self.GetTextRange(pos, next)
        return char

    def GetPrevChar(self, pos=None):
        '''Returns character before pos
        
        This properly handles multibyte characters.
        '''
        if pos is None: pos = self.GetCurrentPos()
        prev = self.PositionBefore(pos)
        char = self.GetTextRange(prev, pos)
        return char

    def GotoBeginningOfLine(self, event):
        self.CmdKeyExecute(wx.stc.STC_CMD_HOME)
        
    def InsertRow(self, pos, data):
        '''Insert a list of strings at the given position
        
        If the position isn't at the start of a line, a newline
        will be inserted before inserting the data so that
        the data comprises a new, complete row.
        '''
        if pos > 0:
            prevchar = self.GetCharAt(self.PositionBefore(pos))
            if prevchar != 10: # newline
                self.InsertText(pos, "\n")
                pos += 1
        cell_separator=u" %s " % SEP
        utext = cell_separator.join([cell.strip() for cell in data]) + u"\n"
        self.InsertTextUTF8(pos, utext.encode("utf-8"))

    def AppendRow(self, data):
        '''Add a row (list of cells) to the end of the widget'''
        if len(data) == 0:
            self.AppendText("\n")
            return

        cell_separator=u" %s " % SEP
        data = [cell.strip() for cell in data]
        if data[-1] != "":
            # make sure there is always a blank cell. It just
            # seems to make life easier
            data.append("")
        utext = cell_separator.join(data) + u"\n"
        self.AppendTextUTF8(utext.encode("utf-8"))

    def OldAppendRow(self, data):
        '''Add a row (list of cells) to the end of the widget'''
        cell_separator=u" %s " % SEP
        utext = cell_separator.join([cell.strip() for cell in data]) + u"\n"
        self.AppendTextUTF8(utext.encode("utf-8"))

    def SetAutoSaveFunction(self, func):
        self._auto_save_func = func

    def GetRows(self):
        '''Retrieve the data as a list of rows, each row a list of cells

        trailing empty cells from each row are stripped
        '''
        data = []
        text = self.GetText().rstrip()
        for line in text.split("\n"):
            line = line.rstrip(u" \t\n%s" % SEP)
            if len(line) >= 0:
                foo = [self._unescape(cell) for cell in line.split(SEP)]
                data.append(foo)
        return data

    def _unescape(self, text):
        '''Massage data before saving it to the model'''
        # 1. replace \\ with some character not otherwise in the string,
        # 2. replace \| with |
        # 3. revert other character back to \\
        text=text.strip().replace(r'\\',u'\u0001').replace(r'\|', '|').replace(u'\u0001', r'\\')
        return text

    def GetCells(self, line=None, indices = False):
        '''Returns the cell data for a given line

        If indices is True, return the cell indices rather than 
        the cell contents
        '''
        if line is None:
            line = self.LineFromPosition(self.GetCurrentPos())
        linestart = self.PositionFromLine(line)
        extents = self._get_cell_ranges(linestart)
        if indices:
            return extents
        else:
            data = [self.GetTextRange(start,end) for (start,end) in extents]

        return [cell.strip() for cell in data]

    def _get_cell_extents(self, pos, trim_whitespace = False):
        '''Returns a two-tuple representing the start and end of the cell containing pos

        If trim_whitespace is true, leading and trailing whitespace of each
        cell is ignored
        '''
        line = self.LineFromPosition(pos)
        linestart = self.PositionFromLine(line)
        lineend = self.GetLineEndPosition(line)
        prev_sep = self.FindText(pos, linestart, SEP)
        if prev_sep == -1: 
            start = linestart
        else:
            start = self.PositionAfter(prev_sep)

        next_sep = self.FindText(pos, lineend, SEP)
        if next_sep == -1: 
            end = lineend
        else:
            end = next_sep

        if trim_whitespace:
            while start >= linestart and self.GetChar(start) == " ":
                start = self.PositionAfter(start)
            while end <= lineend and self.GetPrevChar(end) == " ":
                end = self.PositionBefore(end)
        return (start, max(end, start))
        
    def SelectCurrentCell(self):
        '''Select the text of the current cell, ignoring whitespace'''
        (start, end) = self._get_cell_extents(self.GetCurrentPos(), trim_whitespace=True)
        # SetSelection leaves the cursor at the end; I want the
        # cursor at the beginning. Plus, I don't want to select if there's
        # nothing in the cell
        self.SetCurrentPos(start)
        self.SetAnchor(end)
        
    def OnInsertColumn(self, event):
        '''Insert a column, then move cursor to that column'''
        prev = self.GetPrevChar()
        if prev is not " ":
            self.AddText(u" ")
        self.AddText(SEP + u" ")

    def OnVerticalBar(self, event):
        '''Handle user pressing '|' by adding a column
        
        A column will not be added if the immediately preceeding
        character is a backslash. This also adds whitespace 
        around the column separator.
        '''
        prev = self.GetPrevChar()
        if prev == '\\':
            self.AddText(u"|")
            return
        if prev != " ":
            self.AddText(u" ")
        self.AddText(SEP + u" ")

    def OnShiftTab(self, event):
        '''Move to and select previous cell'''
        self.GotoPos(self._prev_cell())
        self.SelectCurrentCell()

    def OnTab(self, event):
        '''Handle tab press by moving to next cell'''
        pos = self._next_cell()
        self.GotoPos(pos)
        # calling SelectCurrentCell immediately sometimes
        # leaves the cursor at the end of the cell instead
        # of the beginning. I don't know why. Bug in wxPython?
        self.schedule_job(self.SelectCurrentCell)

    def GetCurrentCell(self):
        '''return (line, column) tuple of the cursor position'''
        pos = self.GetCurrentPos()
        extents = self._get_cell_ranges(pos)
        for cell, (start,end) in enumerate(extents):
            if start <= pos <= end:
                return (self.GetCurrentLine(), cell)

        return (self.GetCurrentLine(), 0)
        
    def GetCellValue(self, pos=None):
        '''Return the data within the cell at pos

        If pos is None, use the current position
        '''
        if pos is None:
            pos = self.GetCurrentPos()
        extents = self._get_cell_ranges(pos)
        for i, (start, end) in enumerate(extents):
            if start <= pos <= end:
                return self.GetTextRange(start, end)
        return ""

    def GotoNextCell(self, pos=None):
        '''Move cursor to next cell'''
        if pos is None:
            pos = self.GetCurrentPos()
        self.GotoPos(pos)
        self.GotoPos(self._next_cell())
        self.SelectCurrentCell()

    def OnKeyDown(self, event):
        '''Handle custom bindings defined via a key table'''
        self._auto_save_timer.Stop()
        func = self._key_table(event)
        if func:
            func(event)
        else:
            event.Skip()
        # should we do this on key up instead? Does it really matter?
        self._auto_save_timer.Start(milliseconds=1000, oneShot = True)

    def _prev_cell(self):
        '''Returns the position of the start of the previous cell'''
        line, cell = self.GetCurrentCell()
        if cell == 0:
            if line >= 0:
                line -= 1
            pos = self.PositionFromLine(line)
            extents = self._get_cell_ranges(pos)
            pos = extents[-1][0]
        else:
            cell -= 1
            extents = self._get_cell_ranges(self.GetCurrentPos())
            pos = extents[cell][0]
        return pos
        
    def _next_cell(self):
        '''Returns the position of the start of the next cell'''
        pos = self.GetCurrentPos()
        linestart = self.PositionFromLine(self.GetCurrentLine())
        lineend = self.GetLineEndPosition(self.GetCurrentLine())
        next_sep = self.FindText(pos, lineend, SEP)
        if next_sep == -1:
            # not found; go to next line
            next_line = self.GetCurrentLine()+1
            pos = self.PositionFromLine(next_line)
        else:
            pos = self.PositionAfter(next_sep)
            while self.GetChar(pos) == " ":
                pos = self.PositionAfter(pos)
        return pos


    def AddStyleRegexp(self, start, end, style, regex, flags=0):
        '''Apply a style based on a regular expression.

        start and end represent the range of text to search for the
        pattern. If the pattern has multiple groups, only text that is
        matched in the first group of the regex will be styled.
        '''
        r = re.compile(regex)
        text = self.GetTextRange(start, end)
        matches = 0
        for match in r.finditer(text):
            # N.B. the data in the widget is utf encoded (which means
            # one character may take up more than one byte). So we
            # need to compensate for that when computing the start
            # and end of a match
            uStart = start + len(text[:match.start(0)].encode('utf8'))
            uLen = len(match.group(0).encode('utf8'))
            self.StartStyling(uStart, 0x1f)
            self.SetStyling(uLen, style)
            matches += 1
        return matches
        
# this class only sorta works. I don't think it handles things like
# control-| -- you have to specify control-shift-\ instead. Kinda
# gross. 
class KeyMap(dict):
    """Lookup table for mapping keypress events to functions. """
    def __init__(self, control, handler):
        dict.__init__(self)
        control.Bind(wx.EVT_KEY_DOWN, handler)

    def __call__(self, event):
        """Return the function that maps to the event, or None"""
        keyspec = []
        if event.CmdDown():
            # interesting reading about ControlDown vs. CmdDown
            # http://www.blog.pythonlibrary.org/2009/08/29/wxpython-catching-key-and-char-events/
            keyspec.append("control")
        if event.ShiftDown():
            keyspec.append("shift")
        keyspec.append(event.GetKeyCode())
        for key in [tuple(keyspec), event.GetRawKeyCode(), event.GetUniChar()]:
            if dict.has_key(self, key):
                return dict.get(self, key)
        return None


    def bind(self, *args):
        """Bind a mapping from an event specification to a function

        A specification is the keycode (eg: wx.WXK_TAB) or character
        (e.g. "A") optionally preceeded by "shift" and/or "control". 
        For example:

        table.bind("shift",wx.WXK_TAB, func) # calls func on shift-tab
        table.bind("control","A", func) # calls func on control-a
        """
        if len(args) == 2 and not isinstance(args[0], int):
            # we were given a plain ol' ascii character, use the ordinal
            # value as the key, which should correspond to a raw keycode.
            self.__setitem__(ord(args[0]), args[1])
            return

        keyspec = args[0:-1]
        func = args[-1]
        if not isinstance(keyspec[-1], int):
            # e.g. "A" => 97
            keyspec = keyspec[0:-1] + (ord(keyspec[-1].upper()),)
        hashkey = tuple(keyspec)
        self.__setitem__(hashkey, func)

def column_widths(data):
    '''Return maximum width of each column in a list of lists
    
    The result will be a list of elements equal to the number
    of columns in the longest row in the data

    I think this takes the prize for the most obtuse code
    I've ever written in my life. Inspiration came from
    http://code.activestate.com/recipes/410687
    '''
    column_widths=[[len(cell) for cell in row] for row in data]
    transposed = map(lambda *row: [width or 0 for width in row], *column_widths)
    return [max(columnwidth) for columnwidth in transposed]
    
if __name__ == "__main__":
    app = wx.App()
    frame = wx.Frame(None)
    label = wx.TextCtrl(frame, wx.ID_ANY, style=wx.TE_READONLY)
    dte = DynamicTableCtrl(frame, wx.ID_ANY, size=(500,300))
    sizer = wx.BoxSizer(wx.VERTICAL)
    sizer.Add(label, 0, wx.EXPAND)
    sizer.Add(dte, 1, wx.EXPAND)
    frame.SetSizerAndFit(sizer)
    frame.Show()
    dte.AppendRow(["one","two","three"])
    dte.AppendRow(["A","B", "C", "D"])
    dte.AppendRow(["three"])
    def CellChanged(event):
        global label
        label.SetValue("current cell: %s,%s" % event.current_cell_index)
        print dte.GetRows()
    dte.Bind(EVT_CELL_FOCUS_CHANGED, CellChanged)
    app.MainLoop()

